1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   */
19  package groovy.test;
20  
21  import groovy.lang.Closure;
22  import groovy.lang.GroovyRuntimeException;
23  import groovy.lang.GroovyShell;
24  import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
25  import org.junit.Test;
26  
27  import java.lang.reflect.Method;
28  import java.lang.reflect.Modifier;
29  import java.util.concurrent.atomic.AtomicInteger;
30  import java.util.logging.Logger;
31  
32  /**
33   * <p>{@code GroovyAssert} contains a set of static assertion and test helper methods and is supposed to be a Groovy
34   * extension of JUnit 4's {@link org.junit.Assert} class. In case JUnit 3 is the choice, the {@link groovy.util.GroovyTestCase}
35   * is meant to be used for writing tests based on {@link junit.framework.TestCase}.
36   * </p>
37   *
38   * <p>
39   * {@code GroovyAssert} methods can either be used by fully qualifying the static method like
40   *
41   * <pre>
42   *     groovy.test.GroovyAssert.shouldFail { ... }
43   * </pre>
44   *
45   * or by importing the static methods with one ore more static imports
46   *
47   * <pre>
48   *     import static groovy.test.GroovyAssert.shouldFail
49   *     import static groovy.test.GroovyAssert.assertNotNull
50   * </pre>
51   * </p>
52   *
53   * @see groovy.util.GroovyTestCase
54   *
55   * @author Paul King
56   * @author Andre Steingress
57   *
58   * @since 2.3
59   */
60  public class GroovyAssert extends org.junit.Assert {
61  
62      private static final Logger log = Logger.getLogger(GroovyAssert.class.getName());
63  
64      private static final int MAX_NESTED_EXCEPTIONS = 10;
65      private static final AtomicInteger counter = new AtomicInteger(0);
66  
67      public static final String TEST_SCRIPT_NAME_PREFIX = "TestScript";
68  
69      /**
70       * @return a generic script name to be used by {@code GroovyShell#evaluate} calls.
71       */
72      protected static String genericScriptName() {
73          return TEST_SCRIPT_NAME_PREFIX + (counter.getAndIncrement()) + ".groovy";
74      }
75  
76      /**
77       * Asserts that the script runs without any exceptions
78       *
79       * @param script the script that should pass without any exception thrown
80       */
81      public static void assertScript(final String script) throws Exception {
82          GroovyShell shell = new GroovyShell();
83          shell.evaluate(script, genericScriptName());
84      }
85  
86      /**
87       * Asserts that the given code closure fails when it is evaluated
88       *
89       * @param code the code expected to fail
90       * @return the caught exception
91       */
92      public static Throwable shouldFail(Closure code) {
93          boolean failed = false;
94          Throwable th = null;
95          try {
96              code.call();
97          } catch (GroovyRuntimeException gre) {
98              failed = true;
99              th = ScriptBytecodeAdapter.unwrap(gre);
100         } catch (Throwable e) {
101             failed = true;
102             th = e;
103         }
104         assertTrue("Closure " + code + " should have failed", failed);
105         return th;
106     }
107 
108     /**
109      * Asserts that the given code closure fails when it is evaluated
110      * and that a particular type of exception is thrown.
111      *
112      * @param clazz the class of the expected exception
113      * @param code  the closure that should fail
114      * @return the caught exception
115      */
116     public static Throwable shouldFail(Class clazz, Closure code) {
117         Throwable th = null;
118         try {
119             code.call();
120         } catch (GroovyRuntimeException gre) {
121             th = ScriptBytecodeAdapter.unwrap(gre);
122         } catch (Throwable e) {
123             th = e;
124         }
125 
126         if (th == null) {
127             fail("Closure " + code + " should have failed with an exception of type " + clazz.getName());
128         } else if (!clazz.isInstance(th)) {
129             fail("Closure " + code + " should have failed with an exception of type " + clazz.getName() + ", instead got Exception " + th);
130         }
131         return th;
132     }
133 
134     /**
135      * Asserts that the given code closure fails when it is evaluated
136      * and that a particular Exception type can be attributed to the cause.
137      * The expected exception class is compared recursively with any nested
138      * exceptions using getCause() until either a match is found or no more
139      * nested exceptions exist.
140      * <p>
141      * If a match is found, the matching exception is returned
142      * otherwise the method will fail.
143      *
144      * @param expectedCause the class of the expected exception
145      * @param code          the closure that should fail
146      * @return the cause
147      */
148     public static Throwable shouldFailWithCause(Class expectedCause, Closure code) {
149         if (expectedCause == null) {
150             fail("The expectedCause class cannot be null");
151         }
152         Throwable cause = null;
153         Throwable orig = null;
154         int level = 0;
155         try {
156             code.call();
157         } catch (GroovyRuntimeException gre) {
158             orig = ScriptBytecodeAdapter.unwrap(gre);
159             cause = orig.getCause();
160         } catch (Throwable e) {
161             orig = e;
162             cause = orig.getCause();
163         }
164 
165         if (orig != null && cause == null) {
166             fail("Closure " + code + " was expected to fail due to a nested cause of type " + expectedCause.getName() +
167             " but instead got a direct exception of type " + orig.getClass().getName() + " with no nested cause(s). Code under test has a bug or perhaps you meant shouldFail?");
168         }
169 
170         while (cause != null && !expectedCause.isInstance(cause) && cause != cause.getCause() && level < MAX_NESTED_EXCEPTIONS) {
171             cause = cause.getCause();
172             level++;
173         }
174 
175         if (orig == null) {
176             fail("Closure " + code + " should have failed with an exception having a nested cause of type " + expectedCause.getName());
177         } else if (cause == null || !expectedCause.isInstance(cause)) {
178             fail("Closure " + code + " should have failed with an exception having a nested cause of type " + expectedCause.getName() + ", instead found these Exceptions:\n" + buildExceptionList(orig));
179         }
180         return cause;
181     }
182 
183     /**
184      * Asserts that the given script fails when it is evaluated
185      * and that a particular type of exception is thrown.
186      *
187      * @param clazz the class of the expected exception
188      * @param script  the script that should fail
189      * @return the caught exception
190      */
191     public static Throwable shouldFail(Class clazz, String script) {
192         Throwable th = null;
193         try {
194             GroovyShell shell = new GroovyShell();
195             shell.evaluate(script, genericScriptName());
196         } catch (GroovyRuntimeException gre) {
197             th = ScriptBytecodeAdapter.unwrap(gre);
198         } catch (Throwable e) {
199             th = e;
200         }
201 
202         if (th == null) {
203             fail("Script should have failed with an exception of type " + clazz.getName());
204         } else if (!clazz.isInstance(th)) {
205             fail("Script should have failed with an exception of type " + clazz.getName() + ", instead got Exception " + th);
206         }
207         return th;
208     }
209 
210     /**
211      * Asserts that the given script fails when it is evaluated
212      *
213      * @param script the script expected to fail
214      * @return the caught exception
215      */
216     public static Throwable shouldFail(String script) {
217         boolean failed = false;
218         Throwable th = null;
219         try {
220             GroovyShell shell = new GroovyShell();
221             shell.evaluate(script, genericScriptName());
222         } catch (GroovyRuntimeException gre) {
223             failed = true;
224             th = ScriptBytecodeAdapter.unwrap(gre);
225         } catch (Throwable e) {
226             failed = true;
227             th = e;
228         }
229         assertTrue("Script should have failed", failed);
230         return th;
231     }
232 
233     /**
234      * NotYetImplemented Implementation
235      */
236     private static final ThreadLocal<Boolean> notYetImplementedFlag = new ThreadLocal<Boolean>();
237 
238     /**
239      * From JUnit. Finds from the call stack the active running JUnit test case
240      *
241      * @return the test case method
242      * @throws RuntimeException if no method could be found.
243      */
244     private static Method findRunningJUnitTestMethod(Class caller) {
245         final Class[] args = new Class[]{};
246 
247         // search the initial junit test
248         final Throwable t = new Exception();
249         for (int i = t.getStackTrace().length - 1; i >= 0; --i) {
250             final StackTraceElement element = t.getStackTrace()[i];
251             if (element.getClassName().equals(caller.getName())) {
252                 try {
253                     final Method m = caller.getMethod(element.getMethodName(), args);
254                     if (isPublicTestMethod(m)) {
255                         return m;
256                     }
257                 }
258                 catch (final Exception e) {
259                     // can't access, ignore it
260                 }
261             }
262         }
263         throw new RuntimeException("No JUnit test case method found in call stack");
264     }
265 
266     /**
267      * From Junit. Test if the method is a JUnit 3 or 4 test.
268      *
269      * @param method the method
270      * @return <code>true</code> if this is a junit test.
271      */
272     private static boolean isPublicTestMethod(final Method method) {
273         final String name = method.getName();
274         final Class[] parameters = method.getParameterTypes();
275         final Class returnType = method.getReturnType();
276 
277         return parameters.length == 0
278                 && (name.startsWith("test") || method.getAnnotation(Test.class) != null)
279                 && returnType.equals(Void.TYPE)
280                 && Modifier.isPublic(method.getModifiers());
281     }
282 
283     /**
284      * <p>
285      * Runs the calling JUnit test again and fails only if it unexpectedly runs.<br>
286      * This is helpful for tests that don't currently work but should work one day,
287      * when the tested functionality has been implemented.<br>
288      * </p>
289      *
290      * <p>
291      * The right way to use it for JUnit 3 is:
292      *
293      * <pre>
294      * public void testXXX() {
295      *   if (GroovyTestCase.notYetImplemented(this)) return;
296      *   ... the real (now failing) unit test
297      * }
298      * </pre>
299      *
300      * or for JUnit 4
301      *
302      * <pre>
303      * &#64;Test
304      * public void XXX() {
305      *   if (GroovyTestCase.notYetImplemented(this)) return;
306      *   ... the real (now failing) unit test
307      * }
308      * </pre>
309      * </p>
310      *
311      * <p>
312      * Idea copied from HtmlUnit (many thanks to Marc Guillemot).
313      * Future versions maybe available in the JUnit distribution.
314      * </p>
315      *
316      * @return {@code false} when not itself already in the call stack
317      */
318     public static boolean notYetImplemented(Object caller) {
319         if (notYetImplementedFlag.get() != null) {
320             return false;
321         }
322         notYetImplementedFlag.set(Boolean.TRUE);
323 
324         final Method testMethod = findRunningJUnitTestMethod(caller.getClass());
325         try {
326             log.info("Running " + testMethod.getName() + " as not yet implemented");
327             testMethod.invoke(caller, (Object[]) new Class[]{});
328             fail(testMethod.getName() + " is marked as not yet implemented but passes unexpectedly");
329         }
330         catch (final Exception e) {
331             log.info(testMethod.getName() + " fails which is expected as it is not yet implemented");
332             // method execution failed, it is really "not yet implemented"
333         }
334         finally {
335             notYetImplementedFlag.set(null);
336         }
337         return true;
338     }
339 
340     private static String buildExceptionList(Throwable th) {
341         StringBuilder sb = new StringBuilder();
342         int level = 0;
343         while (th != null) {
344             if (level > 1) {
345                 for (int i = 0; i < level - 1; i++) sb.append("   ");
346             }
347             if (level > 0) sb.append("-> ");
348             if (level > MAX_NESTED_EXCEPTIONS) {
349                 sb.append("...");
350                 break;
351             }
352             sb.append(th.getClass().getName()).append(": ").append(th.getMessage()).append("\n");
353             if (th == th.getCause()) {
354                 break;
355             }
356             th = th.getCause();
357             level++;
358         }
359         return sb.toString();
360     }
361 
362 }